import argparse
import datetime
import logging
import pandas
import pathlib
import re
from typing import Any, Dict, List, Set, Tuple, Type, Optional, Union, Callable
import vnpy.tools.chinese_mainland_business_day
import vnpy.tools.utility
import vnpy.trader.database
from vnpy.trader.constant import Exchange, Interval
from vnpy.trader.object import BarData


def load_ming_xi(
    filepath: Union[str, pathlib.Path],
    business_day: datetime.date,
    symbol: str,
) -> List[dict]:
    """
    载入通达信'明细数据导出'/'分笔'数据文件
    """
    rows: List[dict] = []

    with open(file=filepath, mode="r", encoding="gbk") as f:
        head_title = None

        for line in f.readlines():
            line: str = line.strip()

            fields: List[str] = line.split(sep='\t')
            fields: List[str] = [field.strip() for field in fields]

            if len(fields) == 1:
                continue
            elif len(fields) == 4:
                assert head_title is None
                head_title = fields + ["备注"]
            elif len(fields) == 5:
                row: dict = dict(zip(head_title, fields))
                rows.append(row)
            else:
                assert len(fields) in (1, 4, 5)

    def guess_time(row: dict, business_day: datetime.date) -> datetime.datetime:
        """"""
        # 国内期货, 夜盘从"21:00:00"开始, 导出的数据会有一条"20:59:00", 疑似期货的夜盘集合竞价数据,
        # 需要将其合到"21:00:00"才能生成正确的5秒线,
        if row["时间"] in ("20:59:00", "20:58:59", "20:59:59"):
            row["时间"] = "21:00:00"
        # 国内期货, 如果前一天放假了, 那么这一天是没有夜盘的, 此时,
        # 国内期货, 日盘从"09:00:00"开始, 导出的数据会有一条"08:59:00", 疑似期货的日盘集合竞价数据,
        # 需要将其合到"09:00:00"才能生成正确的5秒线,
        if row["时间"] in ("08:59:00",):
            row["时间"] = "09:00:00"
        # 调整结束时的时间戳,
        if row["时间"] in ("15:00:02",):
            row["时间"] = "14:59:59"

        # 如果昨天放假了, 那么今天是没有夜盘的, 因此没有夜盘数据,
        # 所以 _5_day 和 _6_day 即使计算了也用不上, 这样计算没有什么问题,
        if business_day.weekday() == 0:
            _5_day = business_day + datetime.timedelta(days=-3)
            _6_day = business_day + datetime.timedelta(days=-2)
            _1_day = business_day
        else:
            _5_day = business_day + datetime.timedelta(days=-1)
            _6_day = business_day
            _1_day = business_day

        tm: datetime.time = datetime.time.fromisoformat(row["时间"])

        if 21 <= tm.hour < 24:  # [21:00~24:00]是夜盘的夜里, 可能在周五,
            dttm: datetime.datetime = datetime.datetime.combine(_5_day, tm)
        elif 0 <= tm.hour <= 2:  # [00:00~02:30]是夜盘的凌晨, 可能在周六,
            dttm: datetime.datetime = datetime.datetime.combine(_6_day, tm)
        elif 9 <= tm.hour < 15:  # [09:00~15:00]是白盘, 可能在周一,
            dttm: datetime.datetime = datetime.datetime.combine(_1_day, tm)
        else:
            raise RuntimeError(f'时间[{row["时间"]}]不符合行情时间段的要求')

        return dttm

    exchange: Exchange = vnpy.tools.utility.get_exchange(symbol=symbol)

    open_interest: int = 0
    for row in rows:
        row["市场"] = exchange
        row["代码"] = symbol
        row["时间"] = guess_time(row=row, business_day=business_day)
        row["价格"] = float(row["价格"])
        row["现量"] = float(row["现量"])
        row["仓差"] = float(row["仓差"])
        # 导出的数据"仓差"的值是对的, 但是没有基准值, 所以"持仓"的值是错的,
        # 但是"持仓"的增量即"仓差"又是对的, 而量化程序基本上只会用到"仓差", 所以错误的"持仓"依然有意义,
        open_interest += row["仓差"]
        row["持仓"] = open_interest

    return rows


def to_bar_data(rows: List[dict]) -> List[BarData]:
    """
    dict 转换成 BarData
    """
    # pandas.set_option('display.max_rows', None)  # 显示所有行
    df_raw: pandas.DataFrame = pandas.DataFrame(rows)
    df_raw.set_index("时间", inplace=True, verify_integrity=False)

    # https://zhuanlan.zhihu.com/p/70353374
    # python时序分析之重采集（resample）
    # closed: 向下采样中, 每段时间间隔的哪一段是封闭的,"right"或"left"
    #  label: 向下采样中, 如何使用"right"或"left"的箱标签标记聚合结果, 例如9:30到9:35的五分钟中间隔可以被标记为[9:30到9:35)
    # 如果时间有中断, 中间缺了一些日期等, 用resample会自动填满, 自动填的这些数据的值是NA, dropna刚好删掉它们,
    df_new: pandas.DataFrame = df_raw.resample(rule="5S", closed="left", label="left").aggregate(
        {
            "市场": "first",
            "代码": "first",
            "价格": ["first", "max", "min", "last"],
            "现量": "sum",
            "持仓": "last",
        }
    ).dropna()

    bars: List[BarData] = []

    for idx, row in df_new.iterrows():
        idx: pandas.Timestamp = idx

        bar: BarData = BarData(
            gateway_name="export",
            symbol=czce_4_to_3(exchange=row["市场"]["first"], symbol=row["代码"]["first"]),
            exchange=row["市场"]["first"],
            datetime=vnpy.trader.database.convert_tz(dt=idx.to_pydatetime()),
            interval=Interval.SECOND05,
            volume=row["现量"]["sum"],
            turnover=0,
            open_interest=row["持仓"]["last"],
            open_price=row["价格"]["first"],
            high_price=row["价格"]["max"],
            low_price=row["价格"]["min"],
            close_price=row["价格"]["last"],
        )
        bars.append(bar)

    return bars


def czce_4_to_3(exchange: Exchange, symbol: str) -> str:
    """"""
    if exchange == Exchange.CZCE:
        reMatch: re.Match = re.match(pattern="^(?P<PRODUCT>[a-zA-Z]+)(?P<DATE>[0-9]{4})(?P<OPTION>([cpCP][0-9]+)?)$", string=symbol)
        if reMatch is not None:
            mapping: dict = reMatch.groupdict()
            INT3: int = int(mapping["DATE"]) % 1000  # 三位int值
            symbol: str = f'{mapping["PRODUCT"]}{INT3}{mapping["OPTION"]}'
    return symbol


def process_one_file(dirpath: str, filename: str) -> None:
    """"""
    filepath: pathlib.Path = pathlib.Path(dirpath).joinpath(filename)
    if not (filepath.exists() and filepath.is_file()):
        logging.info(f"[{filename}] 文件不存在, filepath=[{filepath}]")
        logging.info(f"[{len(filename) * '-'}]")
        return

    fields: List[str] = filepath.stem.split(sep='_')
    assert len(fields) == 2

    business_day: datetime.date = datetime.date(
        year=int(fields[0]) // 10000,
        month=int(fields[0]) % 10000 // 100,
        day=int(fields[0]) % 100,
    )
    symbol: str = fields[1]

    logging.info(f"[{filename}] symbol={symbol}, business_day={business_day}")

    rows: List[dict] = load_ming_xi(filepath, business_day=business_day, symbol=symbol)
    logging.info(f"[{filename}] 解析 {len(rows):5d} 条数据")

    bars: List[BarData] = to_bar_data(rows=rows)
    logging.info(f"[{filename}] 生成 {len(bars):5d} 条BarData")

    database: vnpy.trader.database.BaseDatabase = vnpy.trader.database.get_database()
    res: bool = database.save_bar_data(bars=bars)
    logging.info(f"[{filename}] 保存结束, res={res}")
    logging.info(f"[{len(filename) * '-'}]")


def check_generate_filename(filename: str, day: Optional[datetime.date]) -> Optional[str]:
    """
    校验文件名的格式(day是None), 格式不对, 返回None
    校验文件名的格式(day是None), 格式正常, 返回原文件名,
    要生成新的文件名(day非None), 格式不对, 返回None
    要生成新的文件名(day非None), 格式正常, 返回新文件名,
    """
    reMatch: re.Match = re.match(r"^(?P<yyyymmdd>[0-9]{8})_(?P<symbol>[a-zA-Z0-9]+)\.txt$", filename)
    if reMatch is None:  # 格式不对
        return None

    if day:  # 要生成新的文件名
        symbol: str = reMatch.groupdict()["symbol"]
        yyyymmdd: str = day.strftime("%Y%m%d")
        new_name: str = f"{yyyymmdd}_{symbol}.txt"
        return new_name
    else:
        return filename


def main_function(dirpath: str, filename: str, begin: Optional[str], last: Optional[str]):
    """"""
    if not pathlib.Path(dirpath).exists():
        logging.info(f"[检查] 目录不存在, dir=[{dir}]")
        return

    if check_generate_filename(filename=filename, day=None) is None:
        logging.info(f"[检查] 文件名的格式不对, filename=[{filename}]")
        return

    if (begin is None) != (last is None):
        logging.info(f"[检查] begin 和 last 必须同时为空, 或同时有值")
        return

    filename_s: List[str] = []

    if begin and last:
        begin_date: datetime.date = datetime.date.fromisoformat(begin)
        last_date: datetime.date = datetime.date.fromisoformat(last)

        business_day_s = vnpy.tools.chinese_mainland_business_day.ChineseMainlandBusinessDay.calculate_business_day_s(
            begin=begin_date,
            last=last_date,
            count=None,
        )

        for business_day in business_day_s:
            new_name: str = check_generate_filename(filename=filename, day=business_day)
            assert new_name
            filename_s.append(new_name)

    else:
        filename_s.append(filename)

    for filename in filename_s:
        process_one_file(dirpath=dirpath, filename=filename)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")

    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--dir", type=str, default=r"C:\new_tdx\T0002\export", help="文件所在的目录")
    parser.add_argument("-n", "--name", type=str, required=True, help="文件名, 例如: 20240102_MA2405.txt")
    parser.add_argument("-b", "--begin", type=str, help="开始日期(yyyy-mm-dd)")
    parser.add_argument("-l", "--last", type=str, help="最后日期(yyyy-mm-dd)")
    args = parser.parse_args()

    main_function(dirpath=args.dir, filename=args.name, begin=args.begin, last=args.last)
    # DELETE FROM dbbardata WHERE interval='5sec';
    # SELECT DATE(datetime), COUNT(*) FROM dbbardata WHERE interval='5sec' GROUP BY DATE(datetime);
